import threading
import re
import sys
import datetime
import MySQLdb
import settings

class middleware:
    def do_request(self, request):
        request.db = getConnection()
    def do_response(self, request, response):
        if hasattr(request, "db"):
            request.db.close()
            request.db = None
    def do_exception(self, request, response):
        if hasattr(request, "db"):
            request.db.rollback()
            request.db.close()
            request.db = None

class YaapsRecordIter(object):
    def __init__(self, dict, order):
        self.dict = dict
        order.reverse()
        self.keys_in_order = order

    def __iter__(self):
        return self

    def next(self):
        try:
            next_key = self.keys_in_order.pop()
        except:
            raise StopIteration
        return self.dict[next_key]

# The attributes of this class are an underlying dict which represents
# all or a subset of the fields of a record. This class is intended to
# be sub-typed if you want to add record-specific behavior such as
# instance methods or making some fields get re-computed when other
# fields change
#
class YaapsRecord(object):

    def __init__(self,record_as_dict):
        # we have to make sure we don't trigger __setattr__ or __getattr__ before record_as_dict initialized
        self.__dict__['record_as_dict'] = record_as_dict

    def first(self):
        return self.record_as_dict.values()[0]

    def __getstate__(self):
        return object.__getattribute__(self, "__dict__")

    def __setstate__(self,dict):
        object.__setattr__(self, "__dict__", dict)

    def __getattr__(self,key):
        try:
            return self.record_as_dict[key]
        except KeyError:
            object.__getattribute__(self,key)

    def __setattr__(self,key,value):
        if key in self.record_as_dict:
            self.record_as_dict[key] = value
        else:
            object.__setattr__(self,key,value)

    def _set_field_order(self, field_names_in_order):
        # important to copy this since we modify it later
        self.field_names_in_order = field_names_in_order[:]

    def __iter__(self):
        return YaapsRecordIter(self.record_as_dict, self.field_names_in_order)

    def __str__(self):
        return str(self.record_as_dict)

    def __unicode__(self):
        return unicode(self.record_as_dict)

    def __repr__(self):
        return u'<YaapsRecord (%s)>' % self.record_as_dict

    def update_cols(self, record):
        """Updates this record with the columns of the passed in record - like the update method for dicts"""
        self.record_as_dict.update(record.record_as_dict)
        return self

    def copy(self):
        '''Returns a shallow copy of this record - does not maintain any attributes that have been added'''
        dict = self.__dict__['record_as_dict'].copy()
        r = YaapsRecord(dict)
        if hasattr(self, 'field_names_in_order'):
            r._set_field_order([x for x in self.field_names_in_order])
        return r

def assert_generic(cls):
    assert cls is not YaapsRecord and cls is not YaapsGenericRecord, "Cannot call generic methods on YaapsRecord, they require a specific sub class"
    assert hasattr(cls, '_table_name'), "Generic methods can only be called for models with a _table_name attribute.  None for %s" % cls

class YaapsGenericRecord(YaapsRecord):
    @classmethod
    def insert(cls, connection, fields):
        assert_generic(cls)
        if hasattr(cls, 'pre_insert'):
            cls.pre_insert(connection, fields)
        record = generic_insert(connection, cls._table_name, cls, fields)
        if hasattr(cls, 'post_insert'):
            record = cls.post_insert(connection, record)
        return record

    @classmethod
    def query(cls, connection, fields, suffix_sql=''):
        assert_generic(cls)
        return generic_query(connection, cls._table_name, cls, fields, suffix_sql)

    @classmethod
    def get(cls, connection, id):
        assert_generic(cls)
        return generic_get(connection, cls._table_name, cls, id)

    @classmethod
    def update(cls, connection, id, fields):
        assert_generic(cls)
        if hasattr(cls, 'pre_update'):
            cls.pre_update(connection, id, fields)
        generic_update(connection, cls._table_name, id, fields)

    @classmethod
    def delete(cls, connection, id):
        assert_generic(cls)
        generic_delete(connection, cls._table_name, id)

    def __setattr__(self, key, value):
        assert key != '_table_name', "Cannot change the table name of a YaapsGenericRecord class"
        return super(YaapsGenericRecord, self).__setattr__(key, value)

class YaapsResultSet(object):
    def __init__(self,results, result_class=None, field_names_in_order=None, rowcount=None):
        self.results = results
        self.next_result = 0
        self.result_class = YaapsRecord if result_class is None else result_class
        self.field_names_in_order = field_names_in_order
        self.rowcount = rowcount
        self.len_results = None

    def fetchone(self):
        try:
            record = self.result_class(self.results[self.next_result])
            record._set_field_order(self.field_names_in_order)
            self.next_result += 1
            return record
        except IndexError:
            return None

    def fetchall(self):
        records = [self.result_class(r) for r in self.results[self.next_result:]]
        [r._set_field_order(self.field_names_in_order) for r in records]
        self.next_result = len(self.results)
        return records

    def __getitem__(self,index):
        record = self.result_class(self.results[index])
        record._set_field_order(self.field_names_in_order)
        return record

    def __len__(self):
        if self.len_results is None:
            self.len_results = len(self.results)
        return self.len_results

class YaapsConnectionProfiler(object):
    def __init__(self):
        self.reset()

    def start_statement(self):
        self._start = datetime.datetime.now()

    def end_statement(self, statement):
        delta = datetime.datetime.now() - self._start
        self._profiled_statements.append((delta, statement))
        self.total += delta

    def reset(self):
        self._start = None
        self._profiled_statements = []
        self.total = datetime.timedelta(seconds=0)

    def get_statement_profiles(self):
        return self._profiled_statements

class YaapsConnection(object):
    def __init__(self,connect_string, pool, stack_trace = None, profiler_class=None, cache=None):
        m = re.match("mysql://(?P<user>\w+):(?P<passwd>[^@]+)@(?P<host>[\w\-.]+)/(?P<db>[^?]+)\?charset=(?P<charset>utf8)", connect_string)
        if not m:
            raise Exception("Invalid database connect string")
        self.connect_args = m.groupdict()
        self.pool = pool
        self.mysqldb_connection = None
        self.rowcount = 0
        if profiler_class:
            self.profiler = profiler_class()
        else:
            self.profiler = None
        self._stack_trace_where_allocated = stack_trace
        self._last_statement_time = None
        self._needs_commit = False
        self._state = 'open'
        self._open_db_connection_if_needed()

    def reactivate(self, stack_trace = None):
        if self._state != 'closed':
            raise Exception("Connection not closed")
        self._stack_trace_where_allocated = stack_trace
        self._state = 'open'
        self.rowcount = 0
        self.ping()  # will raise if connection not still open

    def execute(self,sql,args=None, mapper=YaapsRecord):
        if self._state != 'open':
            raise Exception("Connection not open")
        self._open_db_connection_if_needed()
        if self.profiler:
            self.profiler.start_statement()
        cursor = self.mysqldb_connection.cursor(MySQLdb.cursors.DictCursor)
        try:
            self.rowcount = cursor.execute(sql,args)
        except MySQLdb.OperationalError, e:
            raise Exception("MySQLdb OperationalError " + e.args[1] + ": " + self._format_sql(sql, args))
        self._needs_commit = True
        self._last_statement_time = datetime.datetime.now()
        results = cursor.fetchall()
        cursor.close()
        if cursor.description:
            field_names_in_order = [d[0] for d in cursor.description]
            if len(set(field_names_in_order)) != len(field_names_in_order):
                raise Exception("Duplicate field names in query. Use 'as' to rename fields.")
        else:
            field_names_in_order = None
        if self.profiler:
            self.profiler.end_statement(self._format_sql(sql, args))
        return YaapsResultSet(results, mapper, field_names_in_order, self.rowcount)

    def get_cursor(self, cursor_type):
        if self._state != 'open':
            raise Exception("Connection not open")
        self._open_db_connection_if_needed()
        cursor = self.mysqldb_connection.cursor(cursor_type)
        return cursor

    def _format_sql(self, sql, args=None):
        if args:
            try:
                stmt = sql % args
            except:
                stmt = "<unprintable sql>"
        else:
            stmt = sql
        return stmt

    def commit(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        if not self._needs_commit:
            return
        self._open_db_connection_if_needed()        
        self.mysqldb_connection.commit()
        self._needs_commit = False

    def rollback(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        self._open_db_connection_if_needed()        
        self.mysqldb_connection.rollback()

    def close(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        self.commit()   # make sure transaction is not left hanging open
        self._stack_trace_where_allocated = None
        if self.profiler:
            self.profiler.reset()
        self._state = 'closed'
        self.pool._put(self)        

    def _close_underlying(self):
        if self.mysqldb_connection:
            self.mysqldb_connection.close()
            self.mysqldb_connection = None
        self._state = 'dead'

    def get_last_insert_id(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        return self.mysqldb_connection.insert_id()

    def _open_db_connection_if_needed(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        if self.mysqldb_connection:
            return
        self.mysqldb_connection = MySQLdb.connect(**self.connect_args)

    def print_stack_trace(self):
        if not self._stack_trace_where_allocated:
            print "No stack trace found"
            raise Exception("No stack trace found")
        frame = self._stack_trace_where_allocated[2]
        print frame.f_code.co_filename + ": " + str(frame.f_lineno)

    def ping(self):
        if self._state != 'open':
            raise Exception("Connection not open")
        if not self.mysqldb_connection:
            return
        now = datetime.datetime.now()
        if not self._last_statement_time or self._last_statement_time < now - datetime.timedelta(seconds=1):
            self.mysqldb_connection.ping()
            self._last_statement_time = now

    def escape(self, text):
        return MySQLdb.escape_string(text)
        
class YaapsConnectionPool(object):
    def __init__(self, connect_string, max_connections, profile=False):
        self.pool = []
        self.all_connections = []
        self.connect_string = connect_string
        self.current_connections = 0
        self.max_connections = max_connections
        self.debug_leaks = settings.DEBUG
        self.profile = profile

    def get(self):
        if self.debug_leaks:
            stack_trace = _recordStackTrace()
            if not stack_trace:
                raise Exception("Failed to record stack trace")
        else:
            stack_trace = None
        with _connection_pool_lock:
            try:
                while True:
                    new_yaaps_connection =  self.pool.pop()
                    try:
                        new_yaaps_connection.reactivate(stack_trace = stack_trace)
                    except:
                        self.current_connections -= 1
                        del self.all_connections[self.all_connections.index(new_yaaps_connection)]
                        new_yaaps_connection._close_underlying()
                        continue
                    return new_yaaps_connection
            except IndexError:
                if self.current_connections >= self.max_connections:
                    if self.debug_leaks:
                        self.printPoolDebugInfo()
                    raise Exception("Too many connections requested from connection pool (pool size = %d, total connections = %d)" % (len(self.pool), self.current_connections))
                if self.profile:
                    profiler_class = YaapsConnectionProfiler
                else:
                    profiler_class = None
                new_yaaps_connection = YaapsConnection(self.connect_string, pool=self, stack_trace=stack_trace, profiler_class=profiler_class)
                self.all_connections.append(new_yaaps_connection)
                self.current_connections += 1
                return new_yaaps_connection

    def _put(self, yaaps_connection):
        with _connection_pool_lock:
            if yaaps_connection not in self.all_connections:
                raise ValueError("Connection not allocated from this pool")
            self.pool.append(yaaps_connection)

    def printPoolDebugInfo(self):
        assert(self.current_connections == len(self.all_connections))        
        print "----"
        print "connect_string = ", self.connect_string
        print "len(self.pool) = ", len(self.pool)
        print "len(self.all_connections) = ", len(self.all_connections)
        print "self.max_connections = ", self.max_connections
        for connection in self.all_connections:
            if connection not in self.pool:
                connection.print_stack_trace()
        print "----"

_connection_pool = {}
_connection_pool_lock = threading.Lock()

def _recordStackTrace():
    trace = []
    depth = 1
    try:
        while (1):
            trace.append(sys._getframe(depth))
            depth += 1
    except:
        if not trace:
            print "Failed to record stack trace"
            raise Exception("Failed to record stack trace")
        return trace

def getConnection(db_id="default"):
    db_id = db_id.lower()
    connect_string = settings.DATABASES[db_id]
    with _connection_pool_lock:
        pool = _connection_pool.setdefault(connect_string, YaapsConnectionPool(connect_string, max_connections = settings.DATABASE_POOL_SIZE, profile=settings.DATABASE_PROFILE))
    return pool.get()

def getMySQLdbConnection(db_id="default"):
    db_id = db_id.lower()
    connect_string = settings.DATABASES[db_id]
    m = re.match("mysql://(?P<user>\w+):(?P<passwd>[^@]+)@(?P<host>[\w\-.]+)/(?P<db>[^?]+)\?charset=(?P<charset>utf8)", connect_string)
    args = m.groupdict()
    return MySQLdb.connect(host = args['host'], user = args['user'], passwd = args['passwd'], charset = args['charset'], db = args['db'])

#
# Generic helper routines for manipulating YaapsRecords or sub-classes
#

def generic_insert(connection, table_name, record_type, fields, delayed=False):
    if delayed:
        sql = "insert delayed into "
    else:
        sql = "insert into "
    connection.execute(sql + table_name + " (%s) values (%s)" % _I(fields), fields)
    if 'id' not in fields:
        fields['id'] = connection.get_last_insert_id()
    return record_type(fields)

def generic_update(connection, table_name, id, fields):
    sql = "update " + table_name + " set " + _U(fields) + " where id = %d" % int(id)
    connection.execute(sql, fields)

def generic_query(connection, table_name, record_type, fields, suffix_sql = ''):
    sql = "select * from " + table_name + " where " + _W(fields) + " " + suffix_sql
    return connection.execute(sql,fields,mapper=record_type)

def generic_get(connection, table_name, record_type, id):
    if id:
        row = generic_query(connection, table_name, record_type, {'id':id}, suffix_sql = "limit 1").fetchone()
        if not row:
            raise IndexError
        else:
            return row
    else:
        return None

def generic_save(connection, table_name, instance):
    generic_update(connection, table_name, instance.id, instance.record_as_dict)

def generic_delete(connection, table_name, id):
    connection.execute("delete from " + table_name + " where id = %s", (id))

#
# Helpers to generate SQL text
#
_reserved_words_to_quote = ['key', 'index']
def _get_names_and_values(d):
    names = []
    values = []

    for key in d.keys():
#        if key in _reserved_words_to_quote:
#            names.append("`" + key + "`")
#        else:
        names.append (key)

    for k in d.keys():
        values.append ("%(" + k + ")s")

    return names, values

def _I(d):
    names, values = _get_names_and_values(d)
    return ("`%s`" % "`,`".join(names), ",".join(values))

def _U(d):
    names, values = _get_names_and_values(d)
    name_value_pairs = ["`" + name + "`=" + value for name, value in zip(names, values)]
    return ",".join(name_value_pairs)

def _W(d):
    names, values = _get_names_and_values(d)
    name_value_pairs = ["`" + name + "`=" + value for name, value in zip(names, values)]
    return " and ".join(name_value_pairs)

#
# Tests
#

if __name__ == '__main__':
    class Items(YaapsRecord):
        def getDisplayName(self):
            return self.name.replace(" ", "-")

    c_orig = c = getConnection()
    rs = c.execute("select * from items limit 2", mapper=Items)
    if len(rs) != 2:
        raise Exception()
    r1 = rs.fetchone()
    r2 = rs.fetchall()[0]
    if r1 is r2:
        raise Exception()
    c.close()
    c = getConnection()
    if c is not c_orig:
        raise Exception()
    c2 = getConnection()
    if c is c2:
        raise Exception()
    failed = False
    try:
        c2.execute("select name, id from topics where name = 'laptops'").fetchone()[0]
    except:
        failed = True
    if not failed:
        raise Exception
    c.close()
    c2.close()
    print "all tests passed"
